package 
{
	import com.adobe.utils.AGALMiniAssembler;
	import com.adobe.utils.PerspectiveMatrix3D;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.display.Stage3D;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.display3D.Context3D;
	import flash.display3D.Context3DProgramType;
	import flash.display3D.Context3DTextureFormat;
	import flash.display3D.Context3DVertexBufferFormat;
	import flash.display3D.IndexBuffer3D;
	import flash.display3D.Program3D;
	import flash.display3D.textures.Texture;
	import flash.display3D.VertexBuffer3D;
	import flash.events.Event;
	import flash.geom.Matrix;
	import flash.geom.Matrix3D;
	import flash.geom.Vector3D;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.utils.getTimer;
	
	/**
	 * ...
	 * @author Olivier Lamontagne
	 */
	public class Main extends Sprite 
	{
		private const SWF_WIDTH:int = 800;
		private const SWF_HEIGHT:int = 600;
		private const TEXTURE_SIZE:int = 512;
		
		private var mFpsLast:uint = getTimer();
		private var mFpsTicks:uint = 0;
		private var mFpsTf:TextField;
		private var mScoreTf:TextField;
		private var mScore:uint = 0;
		
		private var mContext3D:Context3D;
		private var mShaderProgram1:Program3D;
		private var mShaderProgram2:Program3D;
		private var mShaderProgram3:Program3D;
		private var mShaderProgram4:Program3D;
		
		private var mProjectionMatrix:PerspectiveMatrix3D = new PerspectiveMatrix3D();
		private var mModelMatrix:Matrix3D = new Matrix3D();
		private var mViewMatrix:Matrix3D = new Matrix3D();
		private var mTerrainViewMatrix:Matrix3D = new Matrix3D();
		private var mModelViewProjection:Matrix3D = new Matrix3D();
		
		private var mT:Number = 0;
		private var mLoopTemp:int = 0;
		
		[Embed(source="../assets/texture/spaceship_texture.jpg")]
		private var mMyTextureBitmap:Class;
		private var mMyTextureData:Bitmap = new mMyTextureBitmap();
		[Embed(source="../assets/texture/terrain_texture.jpg")]
		private var mMyTerrainTextureBitmap:Class;
		private var mMyTerrainTextureData:Bitmap = new mMyTerrainTextureBitmap();
		
		
		private var mMyTexture:Texture;
		private var mTerrainTexture:Texture;
		
		[Embed(source="../assets/model/spaceship.obj", mimeType = "application/octet-stream")]
		private var mObjClass:Class;
		private var mMyMesh:Stage3DObjParser;
		
		[Embed(source="../assets/model/terrain.obj", mimeType = "application/octet-stream")]
		private var mTerrainObj:Class;
		private var mTerrainMesh:Stage3DObjParser;
		
		public function Main():void 
		{
			if (stage) Init();
			else addEventListener(Event.ADDED_TO_STAGE, Init);
		}
		
		private function Init(aEvent:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
			if (hasEventListener(Event.ADDED_TO_STAGE))
			{
				removeEventListener(Event.ADDED_TO_STAGE, Init);
			}
			
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			
			InitGUI();
			
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, OnCreateContext3D);
			stage.stage3Ds[0].requestContext3D();
		}
		
		private function InitGUI():void
		{
			var myFormat:TextFormat = new TextFormat();
			myFormat.color = 0xFFFFFF;
			myFormat.size = 13;
			
			mFpsTf = new TextField();
			mFpsTf.x = 0;
			mFpsTf.y = 0;
			mFpsTf.selectable = false;
			mFpsTf.autoSize = TextFieldAutoSize.LEFT;
			mFpsTf.defaultTextFormat = myFormat;
			mFpsTf.text = "Initializing Stage3D...";
			addChild(mFpsTf);
			
			mScoreTf = new TextField();
			mScoreTf.x = 560;
			mScoreTf.y = 0;
			mScoreTf.selectable = false;
			mScoreTf.autoSize = TextFieldAutoSize.LEFT;
			mScoreTf.defaultTextFormat = myFormat;
			mScoreTf.text = "000000";
			addChild(mScoreTf);
			
			var label1:TextField = new TextField();
			label1.x = 180;
			label1.y = 240;
			label1.selectable = false;
			label1.autoSize = TextFieldAutoSize.LEFT;
			label1.defaultTextFormat = myFormat;
			label1.text = "Shader 1: Textured";
			addChild(label1);
			
			var label2:TextField = new TextField();
			label2.x = 500;
			label2.y = 240;
			label2.selectable = false;
			label2.autoSize = TextFieldAutoSize.LEFT;
			label2.defaultTextFormat = myFormat;
			label2.text = "Shader 2: Vertex RGB";
			addChild(label2);
			
			var label3:TextField = new TextField();
			label3.x = 140;
			label3.y = 560;
			label3.selectable = false;
			label3.autoSize = TextFieldAutoSize.LEFT;
			label3.defaultTextFormat = myFormat;
			label3.text = "Shader 3: Vertex RGB + Textured";
			addChild(label3);
			
			var label4:TextField = new TextField();
			label4.x = 460;
			label4.y = 560;
			label4.selectable = false;
			label4.autoSize = TextFieldAutoSize.LEFT;
			label4.defaultTextFormat = myFormat;
			label4.text = "Shader 4: Textured + setProgramConstants";
			addChild(label4);
		}
		
		private function OnCreateContext3D(aEvent:Event):void 
		{
			removeEventListener(Event.ENTER_FRAME, Update);
			
			var t:Stage3D = aEvent.target as Stage3D;
			mContext3D = t.context3D;
			
			if (mContext3D == null)
			{
				return;
			}
			
			mContext3D.enableErrorChecking = true;
			
			InitData();
			
			//Configure back buffer for rendering
			mContext3D.configureBackBuffer(SWF_WIDTH, SWF_HEIGHT, 2, true);
			
			InitShaders();
			
			//Create texture with mipmaps
			mMyTexture = mContext3D.createTexture(TEXTURE_SIZE, TEXTURE_SIZE, Context3DTextureFormat.BGRA, false);
			UploadTextureWithMipmaps(mMyTexture, mMyTextureData.bitmapData);
			
			//Create terrain texture
			mTerrainTexture = mContext3D.createTexture(TEXTURE_SIZE, TEXTURE_SIZE, Context3DTextureFormat.BGRA, false);
			UploadTextureWithMipmaps(mTerrainTexture, mMyTerrainTextureData.bitmapData);
			
			
			mProjectionMatrix.identity();
			mProjectionMatrix.perspectiveFieldOfViewRH(45, SWF_WIDTH / SWF_HEIGHT, 0.01, 5000);
			mViewMatrix.identity();
			mViewMatrix.appendTranslation(0, 0, -3);
			
			mTerrainViewMatrix.identity();
			mTerrainViewMatrix.appendRotation( -60, Vector3D.X_AXIS);
			
			addEventListener(Event.ENTER_FRAME, Update);
		}
		
		private function UploadTextureWithMipmaps(aDest:Texture, aSource:BitmapData):void
		{
			var ws:int = mMyTextureData.bitmapData.width;
			var hs:int = mMyTextureData.bitmapData.height;
			var level:int = 0;
			var tmp:BitmapData;
			var transform:Matrix = new Matrix();
			var tmp2:BitmapData;
			
			tmp = new BitmapData(aSource.width, aSource.height, true, 0x00000000);
			while (ws >= 1 && hs >= 1)
			{
				tmp.draw(aSource, transform, null, null, null, true);
				aDest.uploadFromBitmapData(tmp, level);
				transform.scale(0.5, 0.5);
				level++;
				ws >>= 1;
				hs >>= 1;
				if (hs && ws)
				{
					tmp.dispose();
					tmp = new BitmapData(ws, hs, true, 0x00000000);
				}
			}
			tmp.dispose();
		}
		
		private function Update(aEvent:Event):void 
		{
			mContext3D.clear(0, 0, 0);
			
			mT += 2;
			
			RenderTerrain();
			
			var dist:Number = 0.8;
			
			for (mLoopTemp = 0; mLoopTemp < 4; mLoopTemp++)
			{
				mModelMatrix.identity();
				switch(mLoopTemp)
				{
					case 0:
					{
						mContext3D.setTextureAt(0, mMyTexture);
						mContext3D.setProgram(mShaderProgram1);
						mModelMatrix.appendRotation(mT * 0.7, Vector3D.Y_AXIS);
						mModelMatrix.appendRotation(mT * 0.6, Vector3D.X_AXIS);
						mModelMatrix.appendRotation(mT * 1.0, Vector3D.Y_AXIS);
						mModelMatrix.appendTranslation( -dist, dist, 0);
						break;
					}
					case 1:
					{
						mContext3D.setTextureAt(0, null);
						mContext3D.setProgram(mShaderProgram2);
						mModelMatrix.appendRotation(mT * -0.2, Vector3D.Y_AXIS);
						mModelMatrix.appendRotation(mT * 0.4, Vector3D.X_AXIS);
						mModelMatrix.appendRotation(mT * 0.7, Vector3D.Y_AXIS);
						mModelMatrix.appendTranslation( dist, dist, 0);
						break;
					}
					case 2:
					{
						mContext3D.setTextureAt(0, mMyTexture);
						mContext3D.setProgram(mShaderProgram3);
						mModelMatrix.appendRotation(mT * 1.0, Vector3D.Y_AXIS);
						mModelMatrix.appendRotation(mT * -0.2, Vector3D.X_AXIS);
						mModelMatrix.appendRotation(mT * 0.3, Vector3D.Y_AXIS);
						mModelMatrix.appendTranslation( -dist, -dist, 0);
						break;
					}
					case 3:
					{
						mContext3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([1, Math.abs(Math.cos(mT/50)), 0, 1]));
						mContext3D.setTextureAt(0, mMyTexture);
						mContext3D.setProgram(mShaderProgram4);
						mModelMatrix.appendRotation(mT * 0.3, Vector3D.Y_AXIS);
						mModelMatrix.appendRotation(mT * 0.3, Vector3D.X_AXIS);
						mModelMatrix.appendRotation(mT * -0.3, Vector3D.Y_AXIS);
						mModelMatrix.appendTranslation( dist, -dist, 0);
						break;
					}
				}
				
				mModelViewProjection.identity();
				mModelViewProjection.append(mModelMatrix);
				mModelViewProjection.append(mViewMatrix);
				mModelViewProjection.append(mProjectionMatrix);
				
				mContext3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mModelViewProjection, true);
				
				mContext3D.setVertexBufferAt(0, mMyMesh.PositionsBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
				mContext3D.setVertexBufferAt(1, mMyMesh.UvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);
				mContext3D.setVertexBufferAt(2, mMyMesh.ColorsBuffer, 0, Context3DVertexBufferFormat.FLOAT_4);
				
				mContext3D.drawTriangles(mMyMesh.IndexBuffer, 0, mMyMesh.IndexBufferCount);
			}
			mContext3D.present();
			
			mFpsTicks++;
			var now:uint = getTimer();
			var delta:uint = now - mFpsLast;
			if (delta >= 1000)
			{
				var fps:Number = mFpsTicks / delta * 1000;
				mFpsTf.text  = fps.toFixed(1) + " fps";
				mFpsTicks = 0;
				mFpsLast = now;
			}
			
			UpdateScore();
		}
		
		private function InitData():void
		{
			mMyMesh = new Stage3DObjParser(mObjClass, mContext3D, 1, true, true);
			mTerrainMesh = new Stage3DObjParser(mTerrainObj, mContext3D, 1, true, true);
		}
		
		private function InitShaders():void
		{
			var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler();
			vertexShaderAssembler.assemble(Context3DProgramType.VERTEX,
			"m44 op, va0, vc0\n" +
			"mov v0, va0\n" + 
			"mov v1, va1\n" +
			"mov v2, va2\n"
			);
			
			var fragmentShaderAssembler1:AGALMiniAssembler = new AGALMiniAssembler();
			fragmentShaderAssembler1.assemble(Context3DProgramType.FRAGMENT,
			"tex ft0, v1, fs0 <2d,repeat,miplinear>\n" +
			"mov oc, ft0\n"
			);
			
			var fragmentShaderAssembler2:AGALMiniAssembler = new AGALMiniAssembler();
			fragmentShaderAssembler2.assemble(Context3DProgramType.FRAGMENT,
			"mov oc, v2\n"
			);
			
			var fragmentShaderAssembler3:AGALMiniAssembler = new AGALMiniAssembler();
			fragmentShaderAssembler3.assemble(Context3DProgramType.FRAGMENT,
			"tex ft0, v1, fs0 <2d,repeat,miplinear>\n" +
			"mul ft1, v2, ft0\n" +
			"mov oc, ft1\n"
			);
			
			var fragmentShaderAssembler4:AGALMiniAssembler = new AGALMiniAssembler();
			fragmentShaderAssembler4.assemble(Context3DProgramType.FRAGMENT,
			"tex ft0, v1, fs0 <2d,repeat,miplinear>\n" +
			"mul ft1, fc0, ft0\n" + 
			"mov oc, ft1\n"
			);
			
			mShaderProgram1 = mContext3D.createProgram();
			mShaderProgram1.upload(vertexShaderAssembler.agalcode, fragmentShaderAssembler1.agalcode);
			
			mShaderProgram2 = mContext3D.createProgram();
			mShaderProgram2.upload(vertexShaderAssembler.agalcode, fragmentShaderAssembler2.agalcode);
			
			mShaderProgram3 = mContext3D.createProgram();
			mShaderProgram3.upload(vertexShaderAssembler.agalcode, fragmentShaderAssembler3.agalcode);
			
			mShaderProgram4 = mContext3D.createProgram();
			mShaderProgram4.upload(vertexShaderAssembler.agalcode, fragmentShaderAssembler4.agalcode);
		}
		
		private function UpdateScore():void
		{
			mScore++;
			
			if (mScore < 10)
			{
				mScoreTf.text = "Score: 00000" + mScore;
			}
			else if (mScore < 100)
			{
				mScoreTf.text = "Score: 0000" + mScore;
			}
			else if (mScore < 1000)
			{
				mScoreTf.text = "Score: 000" + mScore;
			}
			else if (mScore < 10000)
			{
				mScoreTf.text = "Score: 00" + mScore;
			}
			else if (mScore < 100000)
			{
				mScoreTf.text = "Score: 0" + mScore;
			}
			else if (mScore < 1000000)
			{
				mScoreTf.text = "Score: " + mScore;
			}
		}
		
		private function RenderTerrain():void
		{
			mContext3D.setTextureAt(0, mTerrainTexture);
			mContext3D.setProgram(mShaderProgram1);
			mContext3D.setVertexBufferAt(0, mTerrainMesh.PositionsBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
			mContext3D.setVertexBufferAt(1, mTerrainMesh.UvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);
			mContext3D.setVertexBufferAt(2, mTerrainMesh.ColorsBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
			mModelMatrix.identity();
			mModelMatrix.appendRotation( -90, Vector3D.Y_AXIS);
			mModelMatrix.appendTranslation(Math.cos(mT / 300) * 1000, Math.cos(mT / 200) * 1000 + 500, -130);
			mModelViewProjection.identity();
			mModelViewProjection.append(mModelMatrix);
			mModelViewProjection.append(mTerrainViewMatrix);
			mModelViewProjection.append(mProjectionMatrix);
			mContext3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mModelViewProjection, true);
			mContext3D.drawTriangles(mTerrainMesh.IndexBuffer, 0, mTerrainMesh.IndexBufferCount);
		}
	}
}